Xu hướng mua hàng theo thời điểm trong ngày¶
import numpy as np
import pandas as pd
import plotly.express as px
import seaborn as sns
import matplotlib.pyplot as plt
import plotly.graph_objects as go
from sklearn.preprocessing import MinMaxScaler
from sklearn.cluster import KMeans
try:
df = pd.read_csv('OnlineRetail.csv', encoding='latin-1')
except UnicodeDecodeError:
df = pd.read_csv('OnlineRetail.csv', encoding='windows-1252')
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 541909 entries, 0 to 541908 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 InvoiceNo 541909 non-null object 1 StockCode 541909 non-null object 2 Description 540455 non-null object 3 Quantity 541909 non-null int64 4 InvoiceDate 541909 non-null object 5 UnitPrice 541909 non-null float64 6 CustomerID 406829 non-null float64 7 Country 541909 non-null object dtypes: float64(2), int64(1), object(5) memory usage: 33.1+ MB
df.head()
| InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | CustomerID | Country | |
|---|---|---|---|---|---|---|---|---|
| 0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 1/12/2010 8:26 | 2.55 | 17850.0 | United Kingdom |
| 1 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 1/12/2010 8:26 | 3.39 | 17850.0 | United Kingdom |
| 2 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 1/12/2010 8:26 | 2.75 | 17850.0 | United Kingdom |
| 3 | 536365 | 84029G | KNITTED UNION FLAG HOT WATER BOTTLE | 6 | 1/12/2010 8:26 | 3.39 | 17850.0 | United Kingdom |
| 4 | 536365 | 84029E | RED WOOLLY HOTTIE WHITE HEART. | 6 | 1/12/2010 8:26 | 3.39 | 17850.0 | United Kingdom |
InvoiceNo: Số hóa đơn
StockCode: Mã sản phẩm
Description: Mô tả chi tiết về mặt hàng
Quantity: Số lượng
InvoiceDate: Ngày hóa đơn
UnitPrice: Giá sản phẩm cho mỗi đơn vị hàng hóa tính bằng đơn vị tiền tệ của Anh
CustomerID: Mã khách hàng
Country: Tên quốc gia nơi khách hàng sinh sống hoặc nơi giao dịch được thực hiện
Tiền xử lý dữ liệu¶
df.isna().sum()
InvoiceNo 0 StockCode 0 Description 1454 Quantity 0 InvoiceDate 0 UnitPrice 0 CustomerID 135080 Country 0 dtype: int64
# Loại các dòng có 'CustomerID' và 'Description' là na
df = df.dropna(subset=['CustomerID'])
df = df.dropna(subset=['Description'])
# Định dạng lại cột InvoiceDate
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'], dayfirst=True, format='mixed')
# Loại bỏ các hàng không thể chuyển đổi
df.dropna(subset=['InvoiceDate'], inplace=True)
# Tách Năm
df['Year'] = df['InvoiceDate'].dt.year
# Tách Quý
df['Month'] = df['InvoiceDate'].dt.month
# Tách theo buổi (sáng, chiều, tối)
def get_time_shift(hour):
if 5 <= hour < 12:
return 'Morning'
elif 12 <= hour < 18:
return 'Afternoon'
else:
return 'Evening'
df['Time_Shift'] = df['InvoiceDate'].dt.hour.apply(get_time_shift)
# Lọc bỏ giao dịch bị hủy (InvoiceNo bắt đầu bằng ký tự 'C')
df = df[~df['InvoiceNo'].astype(str).str.contains('C')]
# Lọc bỏ Quantity và UnitPrice không hợp lệ (<0)
df = df[(df['Quantity'] > 0) & (df['UnitPrice'] > 0)]
# Tính tổng giá trị đơn hàng
df['TotalPrice'] = df['Quantity'] * df['UnitPrice']
df.head()
| InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | CustomerID | Country | Year | Month | Time_Shift | TotalPrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 2010-12-01 08:26:00 | 2.55 | 17850.0 | United Kingdom | 2010 | 12 | Morning | 15.30 |
| 1 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom | 2010 | 12 | Morning | 20.34 |
| 2 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 2010-12-01 08:26:00 | 2.75 | 17850.0 | United Kingdom | 2010 | 12 | Morning | 22.00 |
| 3 | 536365 | 84029G | KNITTED UNION FLAG HOT WATER BOTTLE | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom | 2010 | 12 | Morning | 20.34 |
| 4 | 536365 | 84029E | RED WOOLLY HOTTIE WHITE HEART. | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom | 2010 | 12 | Morning | 20.34 |
df.info()
df.isna().sum()
<class 'pandas.core.frame.DataFrame'> Index: 397884 entries, 0 to 541908 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 InvoiceNo 397884 non-null object 1 StockCode 397884 non-null object 2 Description 397884 non-null object 3 Quantity 397884 non-null int64 4 InvoiceDate 397884 non-null datetime64[ns] 5 UnitPrice 397884 non-null float64 6 CustomerID 397884 non-null float64 7 Country 397884 non-null object 8 Year 397884 non-null int32 9 Month 397884 non-null int32 10 Time_Shift 397884 non-null object 11 TotalPrice 397884 non-null float64 dtypes: datetime64[ns](1), float64(3), int32(2), int64(1), object(5) memory usage: 36.4+ MB
InvoiceNo 0 StockCode 0 Description 0 Quantity 0 InvoiceDate 0 UnitPrice 0 CustomerID 0 Country 0 Year 0 Month 0 Time_Shift 0 TotalPrice 0 dtype: int64
EDA¶
# Giao dịch theo buổi
transaction_counts = df['Time_Shift'].value_counts().reset_index()
transaction_counts.columns = ['Time_Shift', 'Count']
fig_bar = px.bar(transaction_counts,
x='Time_Shift',
y='Count',
color_discrete_sequence=['#491D8B'],
title='Số lượng giao dịch theo buổi',
hover_data=['Count'],
width=900,
height=600)
fig_bar.show()
fig_pie = px.pie(transaction_counts,
names='Time_Shift',
values='Count',
color_discrete_sequence=['#491D8B','#7D3AC1','#EB548C'],
title='Tỉ lệ giao dịch theo buổi',
hover_data=['Count'],
width=900,
height=600)
fig_pie.show()
# Tổng tiền trung bình theo buổi
avg_spend = df.groupby('Time_Shift')['TotalPrice'].mean().reset_index()
avg_spend.columns = ['Time_Shift', 'AvgPrice']
fig_bar2 = px.bar(
avg_spend,
x='Time_Shift',
y='AvgPrice',
color_discrete_sequence=['#7D3AC1'],
title='Tổng tiền trung bình theo buổi',
hover_data=['AvgPrice'],
width=900,
height=600
)
fig_bar2.show()
fig_pie2 = px.pie(
avg_spend,
names='Time_Shift',
values='AvgPrice',
color_discrete_sequence=['#491D8B','#7D3AC1','#EB548C'],
title='Tỉ lệ tổng tiền trung bình theo buổi',
hover_data=['AvgPrice'],
width=900,
height=600
)
fig_pie2.show()
# Số khách hàng theo buổi
customer_count = df.groupby('Time_Shift')['CustomerID'].nunique().reset_index()
customer_count.columns = ['Time_Shift', 'CustomerCount']
fig_bar3 = px.bar(
customer_count,
x='Time_Shift',
y='CustomerCount',
color_discrete_sequence=['#EB548C'],
title='Số khách hàng theo buổi',
hover_data=['CustomerCount'],
width=900,
height=600
)
fig_bar3.show()
fig_pie3 = px.pie(
customer_count,
names='Time_Shift',
values='CustomerCount',
color_discrete_sequence=['#491D8B','#7D3AC1','#EB548C'],
title='Tỉ lệ khách hàng theo buổi',
hover_data=['CustomerCount'],
width=900,
height=600
)
fig_pie3.show()
Chuẩn bị dữ liệu cho phân cụm¶
# Số lượng giao dịch theo buổi
pivot_counts = df.pivot_table(
index='CustomerID',
columns='Time_Shift',
values='InvoiceNo',
aggfunc='count',
fill_value=0
)
pivot_counts.columns = [c.lower() + '_count' for c in pivot_counts.columns]
pivot_counts
| afternoon_count | evening_count | morning_count | |
|---|---|---|---|
| CustomerID | |||
| 12346.0 | 0 | 0 | 1 |
| 12347.0 | 136 | 0 | 46 |
| 12348.0 | 3 | 17 | 11 |
| 12349.0 | 0 | 0 | 73 |
| 12350.0 | 17 | 0 | 0 |
| ... | ... | ... | ... |
| 18280.0 | 0 | 0 | 10 |
| 18281.0 | 0 | 0 | 7 |
| 18282.0 | 7 | 0 | 5 |
| 18283.0 | 571 | 87 | 98 |
| 18287.0 | 0 | 0 | 70 |
4338 rows × 3 columns
# Trung bình giá trị hóa đơn theo buổi
pivot_avgprice = df.pivot_table(
index='CustomerID',
columns='Time_Shift',
values='TotalPrice',
aggfunc='mean',
fill_value=0
)
pivot_avgprice.columns = ['avg_totalprice_' + c.lower() for c in pivot_avgprice.columns]
pivot_avgprice
| avg_totalprice_afternoon | avg_totalprice_evening | avg_totalprice_morning | |
|---|---|---|---|
| CustomerID | |||
| 12346.0 | 0.000000 | 0.000000 | 77183.600000 |
| 12347.0 | 22.712059 | 0.000000 | 26.546957 |
| 12348.0 | 103.333333 | 52.517647 | 54.040000 |
| 12349.0 | 0.000000 | 0.000000 | 24.076027 |
| 12350.0 | 19.670588 | 0.000000 | 0.000000 |
| ... | ... | ... | ... |
| 18280.0 | 0.000000 | 0.000000 | 18.060000 |
| 18281.0 | 0.000000 | 0.000000 | 11.545714 |
| 18282.0 | 14.315714 | 0.000000 | 15.568000 |
| 18283.0 | 2.775797 | 2.342644 | 3.123367 |
| 18287.0 | 0.000000 | 0.000000 | 26.246857 |
4338 rows × 3 columns
# Tổng số đơn hàng của khách
total_orders = df.groupby('CustomerID')['InvoiceNo'].nunique().rename('total_orders')
# Tổng chi tiêu của khách
total_spending = df.groupby('CustomerID')['TotalPrice'].sum().rename('total_spending')
customer_features = (
pivot_counts
.join(pivot_avgprice)
.join(total_orders)
.join(total_spending)
)
customer_features.head()
| afternoon_count | evening_count | morning_count | avg_totalprice_afternoon | avg_totalprice_evening | avg_totalprice_morning | total_orders | total_spending | |
|---|---|---|---|---|---|---|---|---|
| CustomerID | ||||||||
| 12346.0 | 0 | 0 | 1 | 0.000000 | 0.000000 | 77183.600000 | 1 | 77183.60 |
| 12347.0 | 136 | 0 | 46 | 22.712059 | 0.000000 | 26.546957 | 7 | 4310.00 |
| 12348.0 | 3 | 17 | 11 | 103.333333 | 52.517647 | 54.040000 | 4 | 1797.24 |
| 12349.0 | 0 | 0 | 73 | 0.000000 | 0.000000 | 24.076027 | 1 | 1757.55 |
| 12350.0 | 17 | 0 | 0 | 19.670588 | 0.000000 | 0.000000 | 1 | 334.40 |
Phân cụm K-Means¶
# Chuẩn hóa dữ liệu
from sklearn.preprocessing import StandardScaler
X = customer_features.copy()
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
sse = []
for i in range(1,11):
kmeans = KMeans(n_clusters=i , max_iter=300)
kmeans.fit(X_scaled)
sse.append(kmeans.inertia_)
fig = px.line(y=sse,template="seaborn",title='Eblow Method')
fig.update_layout(width=800, height=600,
title_font_color="#BF40BF",
xaxis=dict(color="#BF40BF",title="Clusters"),
yaxis=dict(color="#BF40BF",title="SSE"))
Từ k = 1 → 4: SSE giảm mạnh
Từ k = 4 → 5: giảm vừa
Từ k = 5 → 6: gần như không giảm
Từ k = 6 → 9: SSE tiếp tục giảm nhưng rất ít
--> Chọn k = 5
kmeans = KMeans(n_clusters=5, random_state=42)
customer_features['cluster'] = kmeans.fit_predict(X_scaled)
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
principal_components = pca.fit_transform(X_scaled)
customer_features["pca1"] = principal_components[:, 0]
customer_features["pca2"] = principal_components[:, 1]
centroids_reduced = pca.transform(kmeans.cluster_centers_)
cluster_info = [
{'cluster_id': 0, 'color': '#DB4CB2', 'name': 'Cụm 0'},
{'cluster_id': 1, 'color': '#c9e9f6', 'name': 'Cụm 1'},
{'cluster_id': 2, 'color': '#7D3AC1', 'name': 'Cụm 2'},
{'cluster_id': 3, 'color': '#FFC857', 'name': 'Cụm 3'},
{'cluster_id': 4, 'color': '#4CAF50', 'name': 'Cụm 4'}
]
# Tạo figure
fig = go.Figure()
# THÊM CÁC CỤM
for info in cluster_info:
c_id = info['cluster_id']
c_name = info['name']
c_color = info['color']
subset = customer_features[customer_features["cluster"] == c_id]
fig.add_trace(go.Scatter(
x=subset['pca1'],
y=subset['pca2'],
mode='markers',
marker=dict(color=c_color, size=7),
name=c_name,
hovertemplate=
"<b>Khách hàng:</b> %{customdata[0]}<br>" +
"Morning Count: %{customdata[1]}<br>" +
"Afternoon Count: %{customdata[2]}<br>" +
"Evening Count: %{customdata[3]}<br>" +
"Avg Morning: %{customdata[4]:.2f}<br>" +
"Avg Afternoon: %{customdata[5]:.2f}<br>" +
"Avg Evening: %{customdata[6]:.2f}<br>" +
"Total Orders: %{customdata[7]}<br>" +
"Total Spending: %{customdata[8]:.2f}<br>" +
"<extra></extra>",
customdata=subset[
["morning_count", "afternoon_count", "evening_count",
"avg_totalprice_morning", "avg_totalprice_afternoon", "avg_totalprice_evening",
"total_orders", "total_spending"]
].values
))
# THÊM TÂM CỤM
fig.add_trace(go.Scatter(
x=centroids_reduced[:, 0],
y=centroids_reduced[:, 1],
mode='markers',
marker=dict(
color='white',
symbol='star',
size=18,
line=dict(width=2, color='black')
),
name='Tâm Cụm (Centroids)',
hoverinfo='skip'
))
# Layout giống bạn
fig.update_layout(
template='plotly_dark',
width=1000,
height=600,
title='Kết quả phân cụm K-Means khách hàng',
legend_title="Nhóm khách hàng"
)
fig.show()
Cụm 0
- Tập trung sát gốc toạ độ ⇒ hoạt động mua hàng thấp và ổn định.
- Mua hàng rải rác giữa Morning/Afternoon/Evening, nhưng số đơn và chi tiêu thấp.
- Là nhóm đông nhất → khách phổ thông.
Nhóm khách hàng mua ít, chi tiêu thấp, hành vi ổn định – tệp khách phổ biến nhất.
Cụm 1
- Nằm ngang rộng theo trục x (có điểm lên tới x = 40–45).
- Điều này thể hiện tổng chi tiêu hoặc tổng số đơn rất cao.
- Đây là nhóm "đột biến" so với cụm nền (cụm 0 và 4).
- Trải rộng ⇒ mức chi tiêu khách trong cụm này không đều.
Nhóm khách hàng chi tiêu cao / nhiều đơn, nhưng phân tán – có khách chi mạnh, có khách mua nhiều đơn.
Cụm 2
- Cao trên trục y ⇒ hành vi mua hàng đa dạng theo buổi.
- Có sự cân bằng giữa morning–afternoon–evening.
- Chi tiêu trung bình, không quá thấp, không quá cao.
- Nhóm này có hành vi "đa khung giờ".
Nhóm khách mua hàng ở nhiều buổi khác nhau, hoạt động ổn định – tệp khách đa dạng.
Cụm 3
- Được đẩy xa lên phía trục y ⇒ mua vào các buổi khác lệch hẳn (thường Evening).
- Số lượng khách rất ít → đặc tính hành vi độc đáo.
- Có thể chi tiêu trung bình cao hoặc số lần mua đặc biệt nhiều vào một khung giờ.
Nhóm khách có hành vi mua đặc thù (thường mua buổi tối), số đơn không nhiều nhưng chi tiêu khá.
Cụm 4
- Rất nhỏ gọn, nằm gần cụm 0 nhưng lệch nhẹ về 1 hướng.
- Dường như tập trung nhiều hơn vào một buổi cố định (Morning hoặc Afternoon).
- Chi tiêu thấp nhưng có một thói quen mua lặp lại.
Nhóm khách mua đều đặn vào một buổi cố định, nhưng tổng chi tiêu thấp.